Passed
Push — task/application-handle-step-s... ( 5a9035...2b5b8c )
by Yonathan
04:05
created

applicationHooks.tsx ➔ useJob   A

Complexity

Conditions 2

Size

Total Lines 4
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 2
1
/* eslint-disable camelcase */
2
import { useCallback, useEffect, useState } from "react";
3
import { useSelector } from "react-redux";
4
import { DispatchType } from "../configureStore";
5
import { RootState } from "../store/store";
6
import { getAwardRecipientTypes as fetchAwardRecipientTypes } from "../store/AwardRecipientType/awardRecipientTypeActions";
7
import { getAwardRecipientTypes } from "../store/AwardRecipientType/awardRecipientTypeSelector";
8
import { getAwardRecognitionTypes as fetchAwardRecognitionTypes } from "../store/AwardRecognitionType/awardRecognitionTypeActions";
9
import { getAwardRecognitionTypes } from "../store/AwardRecognitionType/awardRecognitionTypeSelector";
10
import { getEducationTypes as fetchEducationTypes } from "../store/EducationType/educationTypeActions";
11
import { getEducationTypes } from "../store/EducationType/educationTypeSelector";
12
import { getEducationStatuses as fetchEducationStatuses } from "../store/EducationStatus/educationStatusActions";
13
import { getEducationStatuses } from "../store/EducationStatus/educationStatusSelector";
14
import {
15
  AwardRecipientType,
16
  AwardRecognitionType,
17
  EducationType,
18
  EducationStatus,
19
  Job,
20
  Experience as ExperienceType,
21
  Skill,
22
  ExperienceSkill,
23
  ApplicationNormalized,
24
  Criteria,
25
  JobPosterQuestion,
26
  JobApplicationAnswer,
27
  Application,
28
  User,
29
} from "../models/types";
30
import {
31
  getApplicationById,
32
  getApplicationIsUpdating,
33
  getApplicationNormalized,
34
  getJobApplicationAnswers,
35
  getJobApplicationSteps,
36
} from "../store/Application/applicationSelector";
37
import { fetchApplication } from "../store/Application/applicationActions";
38
import {
39
  getCriteriaByJob,
40
  getJob,
41
  getJobIsUpdating,
42
  getJobPosterQuestionsByJob,
43
} from "../store/Job/jobSelector";
44
import { fetchJob } from "../store/Job/jobActions";
45
import {
46
  ApplicationStatusId,
47
  ApplicationStep,
48
  ProgressBarStatus,
49
} from "../models/lookupConstants";
50
import {
51
  getExperienceByApplicant,
52
  getExperienceByApplication,
53
  getExperienceSkillsByApplicant,
54
  getExperienceSkillsByApplication,
55
  getUpdatingByApplicant,
56
  getUpdatingByApplication,
57
} from "../store/Experience/experienceSelector";
58
import {
59
  fetchExperienceByApplicant,
60
  fetchExperienceByApplication,
61
} from "../store/Experience/experienceActions";
62
import { getSkills, getSkillsUpdating } from "../store/Skill/skillSelector";
63
import { fetchSkills } from "../store/Skill/skillActions";
64
import { getUserById } from "../store/User/userSelector";
65
import { fetchUser } from "../store/User/userActions";
66
67
export function useUser(userId: number | undefined): User | null {
68
  return useSelector((state: RootState) =>
69
    userId ? getUserById(state, { userId }) : null,
70
  );
71
}
72
export function useApplication(
73
  applicationId: number,
74
): ApplicationNormalized | null {
75
  return useSelector((state: RootState) =>
76
    getApplicationNormalized(state, { applicationId }),
77
  );
78
}
79
80
export function useReviewedApplication(
81
  applicationId: number,
82
): Application | null {
83
  return useSelector((state: RootState) =>
84
    getApplicationById(state, { id: applicationId }),
85
  );
86
}
87
88
export function useJob(jobId: number | undefined): Job | null {
89
  return useSelector((state: RootState) =>
90
    jobId ? getJob(state, { jobId }) : null,
91
  );
92
}
93
94
export function useExperienceConstants(): {
95
  awardRecipientTypes: AwardRecipientType[];
96
  awardRecognitionTypes: AwardRecognitionType[];
97
  educationTypes: EducationType[];
98
  educationStatuses: EducationStatus[];
99
} {
100
  const awardRecipientTypes = useSelector(getAwardRecipientTypes);
101
  const awardRecognitionTypes = useSelector(getAwardRecognitionTypes);
102
  const educationTypes = useSelector(getEducationTypes);
103
  const educationStatuses = useSelector(getEducationStatuses);
104
  return {
105
    awardRecipientTypes,
106
    awardRecognitionTypes,
107
    educationTypes,
108
    educationStatuses,
109
  };
110
}
111
112
export function useSkills(): Skill[] {
113
  return useSelector(getSkills);
114
}
115
116
export function useCriteria(jobId: number | undefined): Criteria[] {
117
  return useSelector((state: RootState) =>
118
    jobId ? getCriteriaByJob(state, { jobId }) : [],
119
  );
120
}
121
122
export function useExperiences(
123
  applicationId: number,
124
  application: ApplicationNormalized | null,
125
): ExperienceType[] {
126
  const applicantId = application?.applicant_id ?? 0;
127
128
  // When an Application is still a draft, use Experiences associated with the applicant profile.
129
  // When an Application has been submitted and is no longer a draft, display Experience associated with the Application directly.
130
  const useProfileExperience =
131
    application === null ||
132
    application.application_status_id === ApplicationStatusId.draft;
133
134
  // This selector must be memoized because getExperienceByApplicant/Application uses reselect, and not re-reselect, so it needs to preserve its state.
135
  const experienceSelector = useCallback(
136
    (state: RootState) =>
137
      useProfileExperience
138
        ? getExperienceByApplicant(state, { applicantId })
139
        : getExperienceByApplication(state, { applicationId }),
140
    [applicationId, applicantId, useProfileExperience],
141
  );
142
  const experiencesByType = useSelector(experienceSelector);
143
  const experiences: ExperienceType[] = [
144
    ...experiencesByType.award,
145
    ...experiencesByType.community,
146
    ...experiencesByType.education,
147
    ...experiencesByType.personal,
148
    ...experiencesByType.work,
149
  ];
150
  return experiences;
151
}
152
153
export function useExperienceSkills(
154
  applicationId: number,
155
  application: ApplicationNormalized | null,
156
): ExperienceSkill[] {
157
  // ExperienceSkills don't need to be fetched because they are returned in the Experiences API calls.
158
  const applicantId = application?.applicant_id ?? 0;
159
  const useProfileExperience =
160
    application === null ||
161
    application.application_status_id === ApplicationStatusId.draft;
162
  const expSkillSelector = (state: RootState) =>
163
    useProfileExperience
164
      ? getExperienceSkillsByApplicant(state, { applicantId })
165
      : getExperienceSkillsByApplication(state, { applicationId });
166
  const experienceSkills = useSelector(expSkillSelector);
167
  return experienceSkills;
168
}
169
170
export function useJobPosterQuestions(
171
  jobId: number | undefined,
172
): JobPosterQuestion[] {
173
  return useSelector((state: RootState) =>
174
    jobId ? getJobPosterQuestionsByJob(state, { jobId }) : [],
175
  );
176
}
177
178
export function useJobApplicationAnswers(
179
  applicationId: number,
180
): JobApplicationAnswer[] {
181
  return useSelector((state: RootState) =>
182
    getJobApplicationAnswers(state, { applicationId }),
183
  );
184
}
185
186
export function useApplicationUser(applicationId: number): User | null {
187
  const application = useApplication(applicationId);
188
  const user = application?.applicant?.user ?? null;
189
  return user;
190
}
191
192
/**
193
 * Return all skills from the redux store, and fetch the skills from backend if they are not yet in the store.
194
 * @param dispatch
195
 */
196
export function useFetchSkills(dispatch: DispatchType): Skill[] {
197
  const skills = useSelector(getSkills);
198
  const skillsUpdating = useSelector(getSkillsUpdating);
199
  useEffect(() => {
200
    if (skills.length === 0 && !skillsUpdating) {
201
      dispatch(fetchSkills());
202
    }
203
  }, [skills.length, skillsUpdating, dispatch]);
204
  return skills;
205
}
206
207
/**
208
 * Return all Experience constants from the redux store, and fetch them from backend if they are not yet in the store.
209
 * @param dispatch
210
 */
211
export function useFetchExperienceConstants(
212
  dispatch: DispatchType,
213
): {
214
  awardRecipientTypes: AwardRecipientType[];
215
  awardRecognitionTypes: AwardRecognitionType[];
216
  educationTypes: EducationType[];
217
  educationStatuses: EducationStatus[];
218
} {
219
  const awardRecipientTypes = useSelector(getAwardRecipientTypes);
220
  const awardRecipientTypesLoading = useSelector(
221
    (state: RootState) => state.awardRecipientType.loading,
222
  );
223
  useEffect(() => {
224
    if (awardRecipientTypes.length === 0 && !awardRecipientTypesLoading) {
225
      dispatch(fetchAwardRecipientTypes());
226
    }
227
  }, [awardRecipientTypes, awardRecipientTypesLoading, dispatch]);
228
229
  const awardRecognitionTypes = useSelector(getAwardRecognitionTypes);
230
  const awardRecognitionTypesLoading = useSelector(
231
    (state: RootState) => state.awardRecognitionType.loading,
232
  );
233
  useEffect(() => {
234
    if (awardRecognitionTypes.length === 0 && !awardRecognitionTypesLoading) {
235
      dispatch(fetchAwardRecognitionTypes());
236
    }
237
  }, [awardRecognitionTypes, awardRecognitionTypesLoading, dispatch]);
238
239
  const educationTypes = useSelector(getEducationTypes);
240
  const educationTypesLoading = useSelector(
241
    (state: RootState) => state.educationType.loading,
242
  );
243
  useEffect(() => {
244
    if (educationTypes.length === 0 && !educationTypesLoading) {
245
      dispatch(fetchEducationTypes());
246
    }
247
  }, [educationTypes, educationTypesLoading, dispatch]);
248
249
  const educationStatuses = useSelector(getEducationStatuses);
250
  const educationStatusesLoading = useSelector(
251
    (state: RootState) => state.educationStatus.loading,
252
  );
253
  useEffect(() => {
254
    if (educationStatuses.length === 0 && !educationStatusesLoading) {
255
      dispatch(fetchEducationStatuses());
256
    }
257
  }, [educationStatuses, educationStatusesLoading, dispatch]);
258
259
  return {
260
    awardRecipientTypes,
261
    awardRecognitionTypes,
262
    educationTypes,
263
    educationStatuses,
264
  };
265
}
266
267
export function useJobApplicationSteps(): {
268
  [step in ApplicationStep]: ProgressBarStatus;
269
} {
270
  return useSelector(getJobApplicationSteps);
271
}
272
273
/**
274
 * Return an Application (normalized, ie without Review) from the redux store, and fetch it from backend if it is not yet in the store.
275
 * @param applicationId
276
 * @param dispatch
277
 */
278
export function useFetchNormalizedApplication(
279
  applicationId: number,
280
  dispatch: DispatchType,
281
): ApplicationNormalized | null {
282
  const applicationSelector = (
283
    state: RootState,
284
  ): ApplicationNormalized | null =>
285
    getApplicationNormalized(state, { applicationId });
286
  const application: ApplicationNormalized | null = useSelector(
287
    applicationSelector,
288
  );
289
  const applicationIsUpdating = useSelector((state: RootState) =>
290
    getApplicationIsUpdating(state, { applicationId }),
291
  );
292
  const [applicationFetched, setApplicationFetched] = useState(false);
293
  useEffect(() => {
294
    if (application === null && !applicationIsUpdating && !applicationFetched) {
295
      setApplicationFetched(true);
296
      dispatch(fetchApplication(applicationId));
297
    }
298
  }, [application, applicationId, applicationIsUpdating, dispatch]);
299
  return application;
300
}
301
302
/**
303
 * Return an Application from the redux store, and fetch it from backend if it is not yet in the store.
304
 * @param applicationId
305
 * @param dispatch
306
 */
307
export function useFetchApplication(
308
  applicationId: number,
309
  dispatch: DispatchType,
310
): Application | null {
311
  const applicationSelector = (state: RootState): Application | null =>
312
    getApplicationById(state, { id: applicationId });
313
  const application: Application | null = useSelector(applicationSelector);
314
  const applicationIsUpdating = useSelector((state: RootState) =>
315
    getApplicationIsUpdating(state, { applicationId }),
316
  );
317
  useEffect(() => {
318
    if (application === null && !applicationIsUpdating) {
319
      dispatch(fetchApplication(applicationId));
320
    }
321
  }, [application, applicationId, applicationIsUpdating, dispatch]);
322
  return application;
323
}
324
325
/**
326
 * Return an Job from the redux store, and fetch it from backend if it is not yet in the store.
327
 * @param jobId
328
 * @param dispatch
329
 */
330
export function useFetchJob(
331
  jobId: number | undefined,
332
  dispatch: DispatchType,
333
): Job | null {
334
  const job = useJob(jobId);
335
  const jobUpdatingSelector = (state: RootState) =>
336
    jobId ? getJobIsUpdating(state, jobId) : false;
337
  const jobIsUpdating = useSelector(jobUpdatingSelector);
338
  useEffect(() => {
339
    // If job is null and not already updating, fetch it.
340
    if (jobId && job === null && !jobIsUpdating) {
341
      dispatch(fetchJob(jobId));
342
    }
343
  }, [jobId, job, jobIsUpdating, dispatch]);
344
  return job;
345
}
346
347
/**
348
 * Return all Experience relavant to an Application from the redux store, and fetch it from backend if it is not yet in the store.
349
 * @param applicationId
350
 * @param application
351
 * @param dispatch
352
 */
353
export function useFetchExperience(
354
  applicationId: number,
355
  application: ApplicationNormalized | null,
356
  dispatch: DispatchType,
357
): {
358
  experiences: ExperienceType[];
359
  experiencesUpdating: boolean;
360
  experiencesFetched: boolean;
361
} {
362
  const applicantId = application?.applicant_id ?? 0;
363
364
  // When an Application is still a draft, use Experiences associated with the applicant profile.
365
  // When an Application has been submitted and is no longer a draft, display Experience associated with the Application directly.
366
  const applicationLoaded = application !== null;
367
  const useProfileExperience =
368
    application === null ||
369
    application.application_status_id === ApplicationStatusId.draft;
370
371
  const experiences = useExperiences(applicationId, application);
372
  const experiencesUpdating = useSelector((state: RootState) =>
373
    useProfileExperience
374
      ? getUpdatingByApplicant(state, { applicantId })
375
      : getUpdatingByApplication(state, { applicationId }),
376
  );
377
  const [experiencesFetched, setExperiencesFetched] = useState(false);
378
  useEffect(() => {
379
    // Only load experiences if they have never been fetched by this component (!experiencesFetched),
380
    //  have never been fetched by another component (length === 0),
381
    //  and are not currently being fetched (!experiencesUpdating).
382
    // Also, wait until application has been loaded so the correct source can be determined.
383
    if (
384
      applicationLoaded &&
385
      !experiencesFetched &&
386
      !experiencesUpdating &&
387
      experiences.length === 0
388
    ) {
389
      setExperiencesFetched(true);
390
      if (useProfileExperience) {
391
        dispatch(fetchExperienceByApplicant(applicantId));
392
      } else {
393
        dispatch(fetchExperienceByApplication(applicationId));
394
      }
395
    }
396
  }, [
397
    applicantId,
398
    applicationId,
399
    applicationLoaded,
400
    dispatch,
401
    experiences.length,
402
    experiencesFetched,
403
    experiencesUpdating,
404
    useProfileExperience,
405
  ]);
406
  return {
407
    experiences,
408
    experiencesUpdating,
409
    experiencesFetched,
410
  };
411
}
412
413
/**
414
 * Return an User from the redux store, and fetch it from backend if it is not yet in the store.
415
 * @param jobId
416
 * @param dispatch
417
 */
418
export function useFetchUser(
419
  userId: number,
420
  dispatch: DispatchType,
421
): User | null {
422
  const user = useUser(userId);
423
  useEffect(() => {
424
    // If job is null and not already updating, fetch it.
425
    if (userId) {
426
      dispatch(fetchUser(userId));
427
    }
428
  }, [userId, dispatch]);
429
  return user;
430
}
431
432
/**
433
 * Trigger fetches for all data needed for the Application process which is not yet in the redux store, or in the process of loading.
434
 * @param applicationId
435
 */
436
export function useFetchAllApplicationData(
437
  applicationId: number,
438
  dispatch: DispatchType,
439
): {
440
  applicationLoaded: boolean;
441
  userLoaded: boolean;
442
  jobLoaded: boolean;
443
  criteriaLoaded: boolean;
444
  experiencesLoaded: boolean;
445
  experienceSkillsLoaded: boolean;
446
  jobQuestionsLoaded: boolean;
447
  applicationAnswersLoaded: boolean;
448
  experienceConstantsLoaded: boolean;
449
  skillsLoaded: boolean;
450
} {
451
  const application = useFetchNormalizedApplication(applicationId, dispatch);
452
  const jobId = application?.job_poster_id;
453
  const job = useFetchJob(jobId, dispatch);
454
  const { experiences, experiencesUpdating } = useFetchExperience(
455
    applicationId,
456
    application,
457
    dispatch,
458
  );
459
  const {
460
    awardRecipientTypes,
461
    awardRecognitionTypes,
462
    educationTypes,
463
    educationStatuses,
464
  } = useFetchExperienceConstants(dispatch);
465
  const skills = useFetchSkills(dispatch);
466
467
  const applicationLoaded = application !== null;
468
  const jobLoaded = job !== null;
469
  const experiencesLoaded = !experiencesUpdating || experiences.length > 0;
470
  const experienceConstantsLoaded =
471
    awardRecipientTypes.length > 0 &&
472
    awardRecognitionTypes.length > 0 &&
473
    educationTypes.length > 0 &&
474
    educationStatuses.length > 0;
475
  const skillsLoaded = skills.length > 0;
476
477
  return {
478
    applicationLoaded,
479
    jobLoaded,
480
    experiencesLoaded,
481
    experienceConstantsLoaded,
482
    skillsLoaded,
483
    criteriaLoaded: jobLoaded,
484
    experienceSkillsLoaded: experiencesLoaded,
485
    jobQuestionsLoaded: jobLoaded,
486
    applicationAnswersLoaded: applicationLoaded,
487
    userLoaded: applicationLoaded,
488
  };
489
}
490
491
/**
492
 * Trigger fetches for all data needed for the Application review process which is not yet in the redux store, or in the process of loading.
493
 * @param applicationId
494
 */
495
export function useFetchReviewApplicationData(
496
  applicantUserId: number,
497
  applicationId: number,
498
  jobId: number,
499
  dispatch: DispatchType,
500
): {
501
  applicationLoaded: boolean;
502
  jobLoaded: boolean;
503
  experiencesLoaded: boolean;
504
  experienceConstantsLoaded: boolean;
505
  skillsLoaded: boolean;
506
} {
507
  const application = useFetchApplication(applicationId, dispatch);
508
  const job = useFetchJob(jobId, dispatch);
509
  const { experiences, experiencesUpdating } = useFetchExperience(
510
    applicationId,
511
    application,
512
    dispatch,
513
  );
514
  const {
515
    awardRecipientTypes,
516
    awardRecognitionTypes,
517
    educationTypes,
518
    educationStatuses,
519
  } = useFetchExperienceConstants(dispatch);
520
  const skills = useFetchSkills(dispatch);
521
522
  return {
523
    applicationLoaded: application !== null,
524
    jobLoaded: job !== null,
525
    experiencesLoaded: !experiencesUpdating || experiences.length > 0,
526
    experienceConstantsLoaded:
527
      awardRecipientTypes.length > 0 &&
528
      awardRecognitionTypes.length > 0 &&
529
      educationTypes.length > 0 &&
530
      educationStatuses.length > 0,
531
    skillsLoaded: skills.length > 0,
532
  };
533
}
534